# coding: utf-8
import datetime
import threading
import time
import random
import re
from queue import Queue

import serial
from apscheduler.schedulers.background import BackgroundScheduler
import adafruit_ssd1306
import board
import cv2
import digitalio
import subprocess
from PIL import Image, ImageDraw, ImageFont, ImageOps

from utils import crc16


class Screen(object):
    def __init__(self, height=64, width=128, fps=20, addr=0x3C, oled_reset=None, content_count=10):
        self.height, self.width, self.fps = height, width, fps
        # Use for I2C.
        # Define the Reset Pin
        oled_reset = digitalio.DigitalInOut(board.D4) if oled_reset is None else oled_reset
        self.oled = adafruit_ssd1306.SSD1306_I2C(self.width, self.height, board.I2C(), addr=addr, reset=oled_reset)
        # an image for screen to show
        self.screen_content = Image.new("1", (self.width, self.height), 0)
        self.draw = ImageDraw.Draw(self.screen_content)
        # a list for users to use
        self.screen_content_list = [0] * content_count
        self.content_lock = threading.Lock()
        self.oled_lock = threading.Lock()
        self.font = ImageFont.truetype("asset/MSYH.TTC", size=20)
        self.need_exit = threading.Event()
        self.update_thread = threading.Thread(target=self.update_thread_func)
        self.update_thread.start()

    def clear(self):
        # Clear display.
        self.oled_lock.acquire()
        self.oled.fill(0)
        self.oled.show()
        self.oled_lock.release()

    def update(self, phase="fore"):
        if phase == "fore":
            # show the picture first
            self.oled_lock.acquire()
            self.oled.image(self.screen_content)
            self.oled.show()
            self.oled_lock.release()
        elif phase == "back":
            # clear the image at first
            self.draw.rectangle((0, 0, self.oled.width, self.oled.height), outline=0, fill=0)
            for idx, item in enumerate(self.screen_content_list):
                if type(item) != list:
                    continue
                if type(item[0]) is str:
                    self.add_text(*item, internal=True)
                if type(item[0]) is Image.Image:
                    self.add_pic(*item, internal=True)
                if type(item[0]) is list:
                    seek_num = self.add_animate(*item, internal=True)
                    self.screen_content_list[idx][4] = seek_num

    def add_animate(self, animation, pos=None, size=None, repeat=True, seek_num=0, internal=False):
        self.content_lock.acquire()
        if internal:
            pic = animation[seek_num]
            self.screen_content.paste(pic, (pos[0], pos[1], pos[0] + size[0], pos[1] + size[1]))
            seek_num = seek_num + 1 if seek_num < len(animation) - 1 else (0 if repeat else seek_num)
            self.content_lock.release()
            return seek_num
        else:
            for idx, value in enumerate(self.screen_content_list):
                if type(value) == int:
                    pic = Image.open(animation) if type(animation) is str else animation
                    size = pic.size if size is None else size
                    pos = (
                        self.oled.width // 2 - size[0] // 2,
                        self.oled.height // 2 - size[1] // 2) if pos is None else pos
                    if (size[0] + pos[0] > self.oled.width) or (size[1] + pos[1] > self.oled.height):
                        rate = min(self.oled.width / size[0], self.oled.height / size[1])
                        size = (int(size[0] * rate), int(size[1] * rate))
                        pos = (self.oled.width // 2 - size[0] // 2, self.oled.height // 2 - size[1] // 2)
                    # load the animation to fit current fps
                    frame_list = self.gif2seq(pic, size)
                    self.screen_content_list[idx] = [frame_list, pos, size, repeat, seek_num]
                    self.content_lock.release()
                    return idx
            self.content_lock.release()
            return -1

    def add_text(self, text, pos=None, font=None, internal=False):
        self.content_lock.acquire()
        if internal:
            self.draw.text(pos, text, font=font, fill=255)
            self.content_lock.release()
            return 0
        else:
            for idx, value in enumerate(self.screen_content_list):
                if type(value) == int:
                    font = font if font is not None else self.font
                    font_width, font_height = font.getsize(text)
                    pos = (self.oled.width // 2 - font_width // 2, self.oled.height // 2 - font_height // 2) \
                        if pos is None else pos
                    self.screen_content_list[idx] = [text, pos, font]
                    self.content_lock.release()
                    return idx
            self.content_lock.release()
            return -1

    def add_pic(self, pic, pos=None, size=None, internal=False):
        self.content_lock.acquire()
        if internal:
            self.screen_content.paste(pic, (pos[0], pos[1], pos[0] + size[0], pos[1] + size[1]))
            self.content_lock.release()
        else:
            for idx, value in enumerate(self.screen_content_list):
                if type(value) == int:
                    pic = Image.open(pic).convert("1") if type(pic) is str else pic
                    size = pic.size if size is None else size
                    pos = (
                        self.oled.width // 2 - size[0] // 2,
                        self.oled.height // 2 - size[1] // 2) if pos is None else pos
                    if (size[0] + pos[0] > self.oled.width) or (size[1] + pos[1] > self.oled.height):
                        rate = min(self.oled.width / size[0], self.oled.height / size[1])
                        pic = pic.resize((int(size[0] * rate), int(size[1] * rate)))
                        pos = (self.oled.width // 2 - size[0] // 2, self.oled.height // 2 - size[1] // 2)
                    else:
                        pic = pic.resize(size)
                    pic = ImageOps.invert(pic.convert("L")).convert("1")
                    self.screen_content_list[idx] = [pic, pos, size]
                    self.content_lock.release()
                    return idx
            self.content_lock.release()
            return -1

    def del_obj(self, idx):
        self.content_lock.acquire()
        self.screen_content_list[idx] = 0
        self.content_lock.release()

    def gif2seq(self, gif, size):
        frame_list = []
        while True:
            try:
                frame_duration = gif.info['duration']  # returns current frame duration in milli sec.
                repeat_times = max(1, int(frame_duration * self.fps / 1000))
                pic = gif.copy().resize(size)
                frame_list += [ImageOps.invert(pic.convert("L")).convert("1") for i in range(repeat_times)]
                gif.seek(gif.tell() + 1)  # image.tell() = current frame
            except EOFError:
                break
            except ValueError:
                print("The given gif is defected!")
                break
        return frame_list

    def update_thread_func(self):
        tic = time.time()
        while not self.need_exit.isSet():
            self.update("back")
            self.update("fore")
            tic += 1 / self.fps
            while time.time() < tic:
                pass

    def start(self):
        self.update_thread.start()

    def stop(self):
        self.need_exit.set()
        self.update_thread.join()


class Camera(object):
    def __init__(self):
        self.cap = cv2.VideoCapture()  # 视频流
        _ = self.cap.open(0)  # 参数是0，表示打开笔记本的内置摄像头，参数是视频文件路径则打开视频
        self.calibration_thread = threading.Thread(target=self.calibration_func)
        self.calibration_thread.start()
        self.camera_lock = threading.Lock()

    def get_frame(self):
        self.camera_lock.acquire()
        flag, image = self.cap.read(0)  # 从视频流中读取
        self.camera_lock.release()
        image = cv2.flip(image, -1)
        return image

    def stop(self):
        self.cap.release()  # 释放视频流

    def calibration_func(self):
        calibration_img = cv2.imread("/home/pi/hhu_water_level/asset/calibration.png")
        idx = -1
        while True:
            img = self.get_frame()
            save_img = img.copy()
            # img[calibration_img[:, :, 2] >= 100] = [0, 0, 255]
            cv2.putText(img, f"img {idx} saved" if idx >=0 else "press s to save", org=(20, 100), fontFace=cv2.FONT_HERSHEY_COMPLEX, fontScale=1, color=(0, 0, 255), thickness=1)
            cv2.imshow("Real Time Capture", img)
            key_code = cv2.waitKey(30)
            if (key_code & 0xFF == ord("s")) or (key_code & 0xFF == ord("S")):
                idx += 1
                cv2.imwrite(f"img_{idx}.png", save_img)

class NetWatcher(object):
    _instance_lock = threading.Lock()

    def __init__(self, screen, command_queue=None):
        self.screen = screen
        self.pattern = re.compile(r'^ttyUSB\d+') # 这是在广域网联通的情况下使用的ping方法，当前通过rtu的方案不适用
        self.network_pics = [f'asset/wifi{i}.png' for i in range(5)]
        self.netowrk_poss = [(76, 0)] + [(75, 0)]*4
        # latency = self.get_delay()
        self.ser = None
        latency = self.connect()
        latency_list = [2000, 1000, 100, 20, 0, latency]
        latency_list.sort(reverse=True)
        self.level = latency_list.index(latency)
        self.command_queue = Queue(maxsize=20)
        self.response_queue = Queue(maxsize=20)
        self.wifi_handler = None
        self.need_display = False
        self.scheduler = BackgroundScheduler()
        self.scheduler.add_job(self.update_network, "interval", seconds=2, id="update_battery")

    def __new__(cls, *args, **kwargs):
        if not hasattr(NetWatcher, "_instance"):
            with NetWatcher._instance_lock:
                if not hasattr(NetWatcher, "_instance"):
                    NetWatcher._instance = object.__new__(cls)
        return NetWatcher._instance

    def connect(self):
        p = subprocess.Popen("ls /dev/ | grep USB", shell=True, stdout=subprocess.PIPE,
                             stderr=subprocess.STDOUT)
        port_str = p.stdout.readlines()[-1].decode("utf-8")
        port = self.pattern.findall(port_str)
        if len(port) == 0:
            print(f"open serial port not found")
            latency = 20210826
            self.ser = None
        else:
            latency = 10
            try:
                port = "/dev/"+port[0]
                self.ser = serial.Serial(port, 9600, timeout=0.5)
            except Exception:
                self.ser = None
                print(f"open serial port {port} filed")
                latency = 20210826
        return latency

    def start_display(self):
        self.need_display = True
        if self.wifi_handler is not None:
            self.screen.del_obj(self.wifi_handler)
        self.wifi_handler = self.screen.add_pic(self.network_pics[self.level], self.netowrk_poss[self.level])
        self.scheduler.start()

    def get_delay(self):
        return 50
        p = subprocess.Popen("ping www.baidu.com -c 4 -W 5", shell=True, stdout=subprocess.PIPE,
                             stderr=subprocess.STDOUT)
        info = p.stdout.readlines()[-1].decode("utf-8")
        result = self.pattern.findall(info)
        if len(result) == 0:
            return 99999
        else:
            return float(result[1][:-1])

    def update_network(self):
        # 尝试恢复连接
        if self.ser is None:
            print("open serial port fialed")
            latency = self.connect()
            if latency > 1000:
                return

        # 读入指令阶段
        try:
            command = self.ser.read_all()
            latency = 5
        except:
            command = ''
            latency = 999999
            self.ser = None
        self.update_screen(latency)
        if command != b'':
            self.command_queue.put(command)

        # 返回指令阶段
        if not self.response_queue.empty():
            try:
                water_level = self.response_queue.get()
                if water_level == -1:
                    # response = b"\x80\x84\x01\xd2\xe8"
                    response = b"\x80\x04\x02\x00\x00iE"
                else:
                    water_level_bytes = int(water_level*100).to_bytes(2, byteorder="big")
                    response = b'\x80\x04\x02' + water_level_bytes
                    response += crc16(response)
                self.ser.write(response)
                latency = 5
            except Exception:
                latency = 999999
                self.ser = None
            self.update_screen(latency)

    def update_screen(self, latency):
        latency_list = [2000, 1000, 100, 20, 0, latency]
        latency_list.sort(reverse=True)
        self.level = latency_list.index(latency)
        if self.need_display:
            if self.wifi_handler is not None:
                self.screen.del_obj(self.wifi_handler)
            self.wifi_handler = self.screen.add_pic(self.network_pics[self.level], self.netowrk_poss[self.level])


class Battery(object):
    _instance_lock = threading.Lock()

    def __init__(self, screen):
        self.screen = screen
        self.font = ImageFont.truetype("asset/MSYH.TTC", size=9)
        self.battery_value = self.get_battery()
        self.scheduler = BackgroundScheduler()
        self.scheduler.add_job(self.update_battery, "interval", seconds=60, id="update_battery")
        self.need_display = False
        self.battery_value_handler = None
        self.battery_pic_handler = None

    def __new__(cls, *args, **kwargs):
        if not hasattr(Battery, "_instance"):
            with Battery._instance_lock:
                if not hasattr(Battery, "_instance"):
                    Battery._instance = object.__new__(cls)
        return Battery._instance

    def get_battery(self):
        return random.randint(1, 100)

    def start_display(self):
        self.need_display = True
        self.update_battery()
        self.scheduler.start()

    def update_battery(self):
        if self.need_display:
            if self.battery_value_handler is not None:
                self.screen.del_obj(self.battery_value_handler)
            if self.battery_pic_handler is not None:
                self.screen.del_obj(self.battery_pic_handler)
            pos = (108, 1) if 10 <= self.battery_value < 100 else ((110, 1) if self.battery_value < 10 else (105, 1))
            self.battery_pic_handler = self.screen.add_pic("asset/battery.png", pos=(100, 0))
            # self.battery_value_handler = self.screen.add_text(str(self.battery_value), pos=pos, font=self.font)
        self.battery_value = self.get_battery()


class Clock(object):
    def __init__(self, screen):
        self.time_text = "0:00:00"
        self.font = ImageFont.truetype("asset/MSYH.TTC", size=15)
        self.screen = screen
        self.need_display = False
        self.time_handler = None
        self.startup_time = datetime.datetime.now()
        self.scheduler = BackgroundScheduler()
        self.scheduler.add_job(self.update_time, "interval", seconds=30, id="update_time")

    def start_display(self):
        self.need_display = True
        self.update_time()
        self.scheduler.start()

    def update_time(self):
        if self.need_display:
            if self.time_handler is not None:
                self.screen.del_obj(self.time_handler)
            self.time_handler = self.screen.add_text(self.time_text, font=self.font, pos=(0, 0))
        time_delta = datetime.datetime.now() - self.startup_time
        time_absolute = datetime.datetime(1970, 1, 1) + time_delta
        self.time_text = f"{time_delta.days}:{time_absolute.hour:02d}:{time_absolute.minute:02d}"


if __name__ == '__main__':
    is_screen_test = False
    is_camera_test = True
    is_battery_test = False
    is_network_test = False
    is_clock_test = False
    if is_screen_test:
        screen = Screen(fps=20)
        screen.clear()
        # pics = ["data/11.gif", "data/img64.gif", "data/animage.gif",
        #         "data/welcome.gif", "data/wel.gif"]
        pics = ["data/badapple.gif"]
        # idx = screen.add_animate(pics[random.randint(0, len(pics) - 1)], repeat=True)
        # idx = screen.add_animate("data/wel.gif", repeat=True)
        # screen.add_pic("asset/wifi0.png")
        # idx = screen.add_text("水位")
        # while True:
        #     for i in range(len(pics)):
        #         idx = screen.add_animate(pics[i])
        #         time.sleep(1000)
        #         screen.del_obj(idx)
        # font = ImageFont.truetype("asset/MSYH.TTC", size=15)
        # screen.add_text("5秒前:22.22cm\n当前:22.22cm", font=font)
        time.sleep(5)
        screen.stop()
    elif is_camera_test:
        camera = Camera()
        calibration_img = cv2.imread("/home/pi/hhu_water_level/asset/calibration.png")
        
        while True:
            img = camera.get_frame()
            img[calibration_img[:,:,2]>=100] = [0, 0, 255]
            cv2.imshow("press Q to quit", img)
            key = cv2.waitKey(30)
            if (key == ord("Q")) or (key == ord("q")):
                break
        cv2.imwrite("test.png", img)
        camera.stop()
    elif is_battery_test:
        screen = Screen()
        battery = Battery(screen=screen)
        time.sleep(100)
        battery.screen.stop()
    elif is_network_test:
        screen = Screen()
        nw = NetWatcher(screen)
        print(nw.get_delay())
        time.sleep(1000)
    elif is_clock_test:
        screen = Screen()
        c = Clock(screen)
        c.start_display()
        time.sleep(1000)
